/*
 *  Copyright (c) 2013 Hai Bison
 *
 *  See the file LICENSE at the root directory of this project for copying
 *  permission.
 */

package com.haibison.android.hutieu;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.WebView;

import com.haibison.android.hutieu.utils.AsyncTaskCompat_v11;
import com.haibison.android.hutieu.utils.MimeTypes;
import com.haibison.android.hutieu.utils.Settings;
import com.haibison.android.hutieu.utils.SimpleWeakEncryption;

/**
 * The share provider.
 * <p/>
 * <h1>Notes</h1>
 * <p/>
 * <ul>
 * <li>Since the purpose of this library is to help you share <i>your own apps'
 * resources</i>, this provider trusts all of your requests. So please make sure
 * they are nice. That means they should point to <i>existing</i> resources...</li>
 * <li><i>For APIs &lt; 9</i>, resources which can be shared <i>directly
 * within</i> a {@link ContentProvider} must <b>not</b> be compressed. That's
 * all official APIs support. However in this provider we use sockets to support
 * for compressed resources. For every request, the provider itself will
 * <i>try</i> to consider the given resource a compressed one <i>if</i> it fails
 * to access the resource at the first time. But that leads to spending more
 * system resources like RAM, CPU... So if the resources are compressed, you
 * should set the parameter {@code compressed} to {@code true}. You'll be more
 * familiar with that parameter while using methods in {@link ShareContract},
 * {@link ShareProviderUtils}...</li>
 * <li>Because of the limit above, for APIs < 9, if you want to use sockets to
 * serve resources, you must use permission {@link Manifest.permission#INTERNET}
 * . Otherwise the provider just returns {@code null} for all requests for
 * compressed resources. About compressed resources, sockets and security: we're
 * using {@link ServerSocket}, {@link Socket} and {@link SimpleWeakEncryption}
 * for exchanging data.</li>
 * <li>Until KitKat (API 19), {@link WebView} still strips out all parameters
 * while processing URLs (<a
 * href="https://code.google.com/p/android/issues/detail?id=62046">issue
 * #62046</a>). But parameters are very important for this provider. So for
 * WebView it might not work in some cases. For example the HTML resources
 * you're trying to give to this provider are compressed, but the WebView will
 * fail to tell the provider about that. However as stated above, this provider
 * will try to consider the given resource a compressed one if it fails to
 * access it at the first time. Then at this moment we think you can use this
 * provider to serve your WebView with compressed resources.</li>
 * </ul>
 * 
 * @author Hai Bison
 */
public class ShareProvider extends ContentProvider {

    private static final String CLASSNAME = ShareProvider.class.getName();

    /**
     * The URI pointing to content item ID.
     */
    public static final int URI_CONTENT_ITEM_ID = 1;

    /**
     * The URI pointing to content item name along with
     * {@link ShareContract#PARAM_RES_ID} <b><i>or</i></b>
     * {@link ShareContract#PARAM_RES_ASSET}.
     */
    public static final int URI_CONTENT_ITEM_NAME = 2;

    /**
     * The URI pointing to content item asset.
     */
    public static final int URI_CONTENT_ITEM_ASSET = 3;

    /**
     * Default openable columns.
     */
    private static final String[] DEFAULT_OPENABLE_COLUMNS = {
            OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };

    /**
     * Default media columns.
     */
    private static final String[] DEFAULT_MEDIA_COLUMNS = {
            MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME,
            MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.SIZE };

    /*
     * HELPER METHODS
     */

    /**
     * Revokes all read/write permissions of a resource ID that you shared
     * before.
     * 
     * @param context
     *            the context.
     * @param resId
     *            the resource ID.
     * @param compressed
     *            specifies whether this resource is compressed or not.
     */
    public static void revokePermissions(Context context, int resId,
            boolean compressed) {
        context.revokeUriPermission(
                ShareContract.genContentIdUri(context, resId, compressed),
                Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    }// revokePermissions()

    private static UriMatcher mUriMatcher;

    /**
     * Gets the URI matcher. You can use it to find URIs of
     * {@code URI_CONTENT_ITEM_*}.
     * 
     * @param context
     *            the context.
     * @return the URI matcher.
     */
    public static UriMatcher getUriMatcher(Context context) {
        if (mUriMatcher == null) {
            mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
            mUriMatcher.addURI(ShareContract.getAuthority(context),
                    ShareContract.PATH_ID + "/#", URI_CONTENT_ITEM_ID);
            mUriMatcher.addURI(ShareContract.getAuthority(context),
                    ShareContract.PATH_NAME + "/*", URI_CONTENT_ITEM_NAME);
            mUriMatcher.addURI(ShareContract.getAuthority(context),
                    ShareContract.PATH_ASSET + "/*", URI_CONTENT_ITEM_ASSET);
        }

        return mUriMatcher;
    }// getUriMatcher()

    /**
     * The instance of {@link ShareProviderServer}. Note that we don't need to
     * close / stop the server. Because every content provider lives last the
     * app's lifetime.
     */
    private ShareProviderServer mShareProviderServer;
    private ContentObserver mContentObserver;

    @Override
    public boolean onCreate() {
        return true;
    }// onCreate()

    @Override
    public String getType(Uri uri) {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME, "getType() >> " + uri);

        final int match = getUriMatcher(getContext()).match(uri);

        switch (match) {
        case URI_CONTENT_ITEM_ID:
        case URI_CONTENT_ITEM_NAME:
        case URI_CONTENT_ITEM_ASSET: {
            String mimeType = ShareProviderUtils.getQueryParameter(uri,
                    ShareContract.PARAM_MIME);
            if (TextUtils.isEmpty(mimeType)) {
                switch (match) {
                case URI_CONTENT_ITEM_ID:
                    mimeType = MimeTypes.MIME_TYPE_UNKNOWN;
                    break;
                case URI_CONTENT_ITEM_NAME:
                case URI_CONTENT_ITEM_ASSET:
                    mimeType = MimeTypes.get(uri);
                    break;
                }
            }// if

            return mimeType;
        }// ALL SUB CONTENT URIS

        default:
            return null;
        }
    }// getType()

    @Override
    public AssetFileDescriptor openAssetFile(Uri uri, String mode)
            throws FileNotFoundException {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME, String.format("openAssetFile() >> uri=%s mode=%s",
                    uri, mode));

        ParcelFileDescriptor parcelFileDescriptor = openFile(uri, mode);
        if (parcelFileDescriptor == null)
            return null;

        final long resSize;
        try {
            resSize = getResourceSize(uri);
            if (resSize < 0)
                return null;
            return new AssetFileDescriptor(parcelFileDescriptor, 0, resSize);
        } catch (IOException e) {
            Log.e(CLASSNAME, e.getMessage(), e);
            return null;
        }
    }// openAssetFile()

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode)
            throws FileNotFoundException {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME,
                    String.format("openFile() >> uri=%s mode=%s", uri, mode));

        final boolean useSocket = ShareProviderUtils.getBooleanQueryParameter(
                uri, ShareContract.PARAM_USE_SOCKET);
        int resId;
        String assetPath;

        switch (getUriMatcher(getContext()).match(uri)) {
        case URI_CONTENT_ITEM_ID: {
            if (useSocket)
                return sendToShareProviderServer(uri);

            /*
             * The URI matcher guarantees `parseInt()`.
             */
            resId = Integer.parseInt(uri.getLastPathSegment());

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
                return createParcelFileDescriptor_v9(resId);

            if (ShareProviderUtils.getBooleanQueryParameter(uri,
                    ShareContract.PARAM_COMPRESSED))
                return sendToShareProviderServer(uri);

            try {
                return getContext().getResources().openRawResourceFd(resId)
                        .getParcelFileDescriptor();
            } catch (Resources.NotFoundException e) {
                /*
                 * Perhaps the resource given is a compressed one...
                 */
                return sendToShareProviderServer(uri);
            }
        }// URI_CONTENT_ITEM_ID

        case URI_CONTENT_ITEM_NAME: {
            if (useSocket)
                return sendToShareProviderServer(uri);

            assetPath = ShareProviderUtils.getQueryParameter(uri,
                    ShareContract.PARAM_RES_ASSET);
            if (TextUtils.isEmpty(assetPath)) {
                /*
                 * The callers must take care of PARAM_RES_ID themselves.
                 */
                resId = Integer.parseInt(ShareProviderUtils.getQueryParameter(
                        uri, ShareContract.PARAM_RES_ID));

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
                    return createParcelFileDescriptor_v9(resId);

                if (ShareProviderUtils.getBooleanQueryParameter(uri,
                        ShareContract.PARAM_COMPRESSED))
                    return sendToShareProviderServer(uri);

                try {
                    return getContext().getResources().openRawResourceFd(resId)
                            .getParcelFileDescriptor();
                } catch (Resources.NotFoundException e) {
                    /*
                     * Perhaps the resource given is a compressed one...
                     */
                    return sendToShareProviderServer(uri);
                }
            }// ASSET PATH IS EMPTY
            else {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
                    return createParcelFileDescriptor_v9(assetPath);

                if (ShareProviderUtils.getBooleanQueryParameter(uri,
                        ShareContract.PARAM_COMPRESSED))
                    return sendToShareProviderServer(uri);

                try {
                    return getContext().getAssets().openFd(assetPath)
                            .getParcelFileDescriptor();
                } catch (Resources.NotFoundException e) {
                    /*
                     * Perhaps the resource given is a compressed one...
                     */
                    return sendToShareProviderServer(uri);
                } catch (IOException e) {
                    /*
                     * Perhaps the resource given is a compressed one...
                     */
                    return sendToShareProviderServer(uri);
                }
            }// ASSET PATH IS NOT EMPTY
        }// URI_CONTENT_ITEM_NAME

        case URI_CONTENT_ITEM_ASSET: {
            if (useSocket)
                return sendToShareProviderServer(uri);

            assetPath = uri.getLastPathSegment();

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
                return createParcelFileDescriptor_v9(assetPath);

            if (ShareProviderUtils.getBooleanQueryParameter(uri,
                    ShareContract.PARAM_COMPRESSED))
                return sendToShareProviderServer(uri);

            try {
                return getContext().getAssets().openFd(assetPath)
                        .getParcelFileDescriptor();
            } catch (Resources.NotFoundException e) {
                /*
                 * Perhaps the resource given is a compressed one...
                 */
                return sendToShareProviderServer(uri);
            } catch (IOException e) {
                /*
                 * Perhaps the resource given is a compressed one...
                 */
                return sendToShareProviderServer(uri);
            }
        }// URI_CONTENT_ITEM_ASSET

        default:
            return null;
        }
    }// openFile()

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        /*
         * Do nothing.
         */
        return 0;
    }// delete()

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        /*
         * Do nothing.
         */
        return null;
    }// insert()

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME, "query() >> " + uri);

        projection = resolveProjection(uri, projection);

        final MatrixCursor result = new MatrixCursor(projection);
        /*
         * Sometimes the input orders are different to the internal orders. This
         * could be an Android's bug. So we obtain the columns again.
         */
        projection = result.getColumnNames();
        RowBuilder newRow = result.newRow();

        for (String col : projection) {
            if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                switch (getUriMatcher(getContext()).match(uri)) {
                case URI_CONTENT_ITEM_ID: {
                    /*
                     * We shouldn't reveal the resource ID.
                     */
                    newRow.add(ShareProviderUtils.getQueryParameter(uri,
                            ContentResolver.SCHEME_CONTENT));
                    break;
                }// URI_CONTENT_ITEM_ID

                case URI_CONTENT_ITEM_NAME: {
                    newRow.add(uri.getLastPathSegment());
                    break;
                }// URI_CONTENT_ITEM_NAME

                case URI_CONTENT_ITEM_ASSET: {
                    newRow.add(new File(uri.getLastPathSegment()).getName());
                    break;
                }// URI_CONTENT_ITEM_ASSET

                default:
                    return null;
                }
            }// DISPLAY_NAME
            else if (OpenableColumns.SIZE.equals(col)) {
                try {
                    newRow.add(getResourceSize(uri));
                } catch (IOException e) {
                    newRow.add(null);
                }
            }// SIZE
            else if (MediaStore.MediaColumns.DATA.equals(col)) {
                newRow.add(uri);
            }// DATA
            else if (MediaStore.MediaColumns.MIME_TYPE.equals(col)) {
                newRow.add(getType(uri));
            }// MIME_TYPE
        }// for

        return result;
    }// query()

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        /*
         * Do nothing.
         */
        return 0;
    }// update()

    /**
     * Checks to see if the share provider server is available or not. If not
     * yet, tries to create it.
     * 
     * @return {@code true} if the server is available, {@code false} otherwise.
     */
    private synchronized boolean checkShareProviderServer() {
        if (!Settings
                .hasPermissions(getContext(), Manifest.permission.INTERNET))
            return false;

        if (mShareProviderServer == null || mShareProviderServer.isClosed()) {
            try {
                mShareProviderServer = new ShareProviderServer(getContext(),
                        Settings.getServerMaxConcurrentThreads(getContext()),
                        Settings.getServerMaxIdleTime(getContext())) {

                    @Override
                    public synchronized void close() throws IOException {
                        if (mContentObserver != null) {
                            getContext()
                                    .getContentResolver()
                                    .unregisterContentObserver(mContentObserver);
                            mContentObserver = null;
                        }

                        mShareProviderServer = null;
                        super.close();
                    }// close()

                };

                if (mContentObserver == null) {
                    mContentObserver = new ContentObserver(null) {

                        @Override
                        public void onChange(boolean selfChange) {
                            onChange(selfChange, null);
                        }// onChange()

                        /*
                         * Remove this annotation for API 4.
                         */
                        // @Override
                        public void onChange(boolean selfChange, Uri uri) {
                            if (BuildConfig.DEBUG)
                                Log.d(CLASSNAME,
                                        String.format(
                                                "mContentObserver.onChange() >> self=%s uri=%s",
                                                selfChange, uri));

                            if (uri != null
                                    && !ShareProviderUtils
                                            .validatePassport(
                                                    getContext(),
                                                    uri.getQueryParameter(ShareContract.PARAM_PASSPORT)))
                                return;

                            if (BuildConfig.DEBUG && uri != null)
                                Log.d(CLASSNAME, "\t> passport validated");

                            final ShareProviderServer _ = mShareProviderServer;
                            if (_ != null) {
                                _.setMaxConcurrentThreads(Settings
                                        .getServerMaxConcurrentThreads(getContext()));
                                _.setMaxIdleTime(Settings
                                        .getServerMaxIdleTime(getContext()));
                            }
                        }// onChange()

                    };

                    getContext()
                            .getContentResolver()
                            .registerContentObserver(
                                    ShareContract
                                            .genContentSettingsUriBase(getContext()),
                                    true, mContentObserver);
                }// if
            } catch (IOException e) {
                Log.e(CLASSNAME, e.getMessage(), e);
                return false;
            }
        }// if

        return true;
    }// checkShareProviderServer()

    /**
     * Sends a resource URI to {@link ShareProviderServer}.
     * 
     * @param uri
     *            the resource URI.
     * @return the instance of {@link AssetFileDescriptor} in which is used for
     *         {@link #openAssetFile(Uri, String)}, or {@code null} if an error
     *         occurred.
     */
    @SuppressLint("InlinedApi")
    private ParcelFileDescriptor sendToShareProviderServer(Uri uri) {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME, "sendToShareProviderServer() >> " + uri);

        if (!checkShareProviderServer())
            return null;

        final String encryptedUri = SimpleWeakEncryption.encrypt(
                mShareProviderServer.getPassword(),
                mShareProviderServer.getSalt(), uri.toString());

        /*
         * Use thread to avoid of NetworkOnMainThreadException.
         */
        final Socket[] sockets = { null };
        AsyncTask<Void, Void, Socket> task = new AsyncTask<Void, Void, Socket>() {

            @Override
            protected Socket doInBackground(Void... params) {
                try {
                    sockets[0] = new Socket(
                            mShareProviderServer.getInetAddress(),
                            mShareProviderServer.getLocalPort());

                    /*
                     * Send the encrypted URI.
                     */
                    sockets[0].getOutputStream().write(encryptedUri.getBytes());
                    sockets[0].getOutputStream().write(0);
                    sockets[0].getOutputStream().flush();
                } catch (IOException e) {
                    Log.e(CLASSNAME, e.getMessage(), e);
                    sockets[0] = null;
                }

                return sockets[0];
            }// doInBackground()

            @Override
            protected void onPostExecute(Socket result) {
                if (isCancelled())
                    onCancelled(result);
            }// onPostExecute()

            @Override
            protected void onCancelled() {
                onCancelled(sockets[0]);
            }// onCancelled()

            /*
             * Remove this annotation to support API 4.
             */
            // @Override
            protected void onCancelled(Socket result) {
                if (result != null) {
                    try {
                        result.close();
                    } catch (IOException e) {
                        Log.e(CLASSNAME, e.getMessage(), e);
                    }
                }
            }// onCancelled()

        };
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
            task.execute();
        else
            AsyncTaskCompat_v11.executeOnExecutor(task,
                    AsyncTaskCompat_v11.Executor.THREAD_POOL);

        /*
         * We shouldn't worry about waiting for the task here... Indeed, if we
         * don't use async task, we still have to wait for the socket client to
         * successfully create the connection.
         */
        final int delay = 10;
        int totalDelayTime = 0;
        while (sockets[0] == null
                && totalDelayTime < DateUtils.SECOND_IN_MILLIS) {
            try {
                Thread.sleep(delay);
                totalDelayTime += delay;
            } catch (InterruptedException e) {
                task.cancel(true);
                Log.e(CLASSNAME, e.getMessage(), e);
                return null;
            }
        }

        if (sockets[0] == null) {
            if (BuildConfig.DEBUG)
                Log.d(CLASSNAME, "\t>> sockets[0] == null");
            return null;
        }

        /*
         * Return the data.
         */
        return ParcelFileDescriptor.fromSocket(sockets[0]);
    }// sendToShareProviderServer()

    /**
     * Creates new resource reader for a resource ID.
     * 
     * @param resId
     *            the resource ID.
     * @return the resource reader, or {@code null} if an error occurred.
     */
    private ParcelFileDescriptor createParcelFileDescriptor_v9(int resId) {
        try {
            return ShareProviderCompat_v9.createStreamTransfer(getContext()
                    .getResources().openRawResource(resId));
        } catch (IOException e) {
            Log.e(CLASSNAME, e.getMessage(), e);
            return null;
        }
    }// createParcelFileDescriptor_v9()

    /**
     * Creates new resource reader for an asset resource.
     * 
     * @param assetPath
     *            the asset resource.
     * @return the resource reader, or {@code null} if an error occurred.
     */
    private ParcelFileDescriptor createParcelFileDescriptor_v9(String assetPath) {
        try {
            return ShareProviderCompat_v9.createStreamTransfer(getContext()
                    .getAssets().open(assetPath));
        } catch (IOException e) {
            Log.e(CLASSNAME, e.getMessage(), e);
            return null;
        }
    }// createParcelFileDescriptor_v9()

    /**
     * Gets resource size.
     * 
     * @param uri
     *            the resource URI.
     * @return the resource size, or {@code -1} if the given URI is not
     *         supported.
     * @throws IOException
     *             if any.
     */
    private long getResourceSize(Uri uri) throws IOException {
        switch (getUriMatcher(getContext()).match(uri)) {
        case URI_CONTENT_ITEM_ID: {
            /*
             * The URI matcher guarantees `parseInt()`.
             */
            return getResourceSize(Integer.parseInt(uri.getLastPathSegment()));
        }// URI_CONTENT_ITEM_ID

        case URI_CONTENT_ITEM_NAME: {
            String assetPath = ShareProviderUtils.getQueryParameter(uri,
                    ShareContract.PARAM_RES_ASSET);
            if (TextUtils.isEmpty(assetPath)) {
                /*
                 * The callers must take care of PARAM_RES_ID themselves.
                 */
                return getResourceSize(Integer.parseInt(ShareProviderUtils
                        .getQueryParameter(uri, ShareContract.PARAM_RES_ID)));
            } else {
                /*
                 * The callers must take care of the path themselves.
                 */
                return getResourceSize(assetPath);
            }
        }// URI_CONTENT_ITEM_NAME

        case URI_CONTENT_ITEM_ASSET: {
            /*
             * The callers must take care of the path themselves.
             */
            return getResourceSize(uri.getLastPathSegment());
        }// URI_CONTENT_ITEM_ASSET

        default:
            return -1;
        }
    }// getResourceSize()

    /**
     * Gets resource size.
     * 
     * @param resId
     *            the resource ID.
     * @return the resource size.
     * @throws IOException
     *             if any.
     */
    private long getResourceSize(int resId) throws IOException {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME, String.format("getResourceSize() >> 0x%x", resId));

        InputStream inputStream = getContext().getResources().openRawResource(
                resId);
        try {
            if (BuildConfig.DEBUG)
                Log.d(CLASSNAME, String.format("\t> size = %,d",
                        inputStream.available()));
            return inputStream.available();
        } finally {
            inputStream.close();
        }
    }// getResourceSize()

    /**
     * Gets resource size.
     * 
     * @param assetPath
     *            the asset path of a resource.
     * @return the resource size.
     * @throws IOException
     *             if any.
     */
    private long getResourceSize(String assetPath) throws IOException {
        if (BuildConfig.DEBUG)
            Log.d(CLASSNAME,
                    String.format("getResourceSize() >> %s", assetPath));

        InputStream inputStream = getContext().getAssets().open(assetPath);
        try {
            if (BuildConfig.DEBUG)
                Log.d(CLASSNAME, String.format("\t> size = %,d",
                        inputStream.available()));
            return inputStream.available();
        } finally {
            inputStream.close();
        }
    }// getResourceSize()

    /**
     * Resolves projection.
     * 
     * @param uri
     *            the URI.
     * @param projection
     *            the projection.
     * @return the resolved projection.
     */
    private String[] resolveProjection(Uri uri, String[] projection) {
        final String mimeType = getType(uri);
        if (TextUtils.isEmpty(mimeType))
            return projection == null ? DEFAULT_OPENABLE_COLUMNS : projection;

        if (mimeType.startsWith(MimeTypes.PREFIX_MIME_TYPE_AUDIO)
                || mimeType.startsWith(MimeTypes.PREFIX_MIME_TYPE_IMAGE)
                || mimeType.startsWith(MimeTypes.PREFIX_MIME_TYPE_VIDEO))
            return projection == null ? DEFAULT_MEDIA_COLUMNS : projection;

        return projection == null ? DEFAULT_OPENABLE_COLUMNS : projection;
    }// resolveProjection()

}
